/*
 * Copyright 2020 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.seppiko.chart.services;

import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.client.result.ResultParser;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.multi.GenericMultipleBarcodeReader;
import com.google.zxing.multi.MultipleBarcodeReader;
import java.awt.image.BufferedImage;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.EnumSet;
import java.util.LinkedHashMap;
import java.util.Map;
import org.seppiko.chart.models.ControllerEntity;
import org.seppiko.chart.models.MessageEntity;
import org.seppiko.chart.utils.ImageUtil;
import org.seppiko.chart.utils.JsonUtil;
import org.seppiko.chart.utils.logging.LoggingManager;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;

/**
 * @author Leonard Woo
 */
@Service
public class DecodeService {

  private static final LoggingManager Logger = LoggingManager.getInstance().getLogger(DecodeService.class.getName());

  public ControllerEntity decodeCHX(InputStream is) {
    ControllerEntity resp = new ControllerEntity();
    resp.setStatus(200);
    resp.setMediaType(MediaType.APPLICATION_JSON);
    BufferedImage image = ImageUtil.processStream(is);
    try {
      if (image == null) {
        resp.setStatus(400);
        resp.setData( JsonUtil.toJson(
            new MessageEntity(400, "barcode decode failed.") ).getBytes() );
      } else {
        LinkedHashMap<String, Object> json = new LinkedHashMap<>();
        ArrayList<Result> results = processImage(image);
        if (results.isEmpty()) {
          resp.setStatus(404);
          resp.setData( JsonUtil.toJson(
              new MessageEntity(404, "Cannot found any barcode.") ).getBytes() );
        } else {
          for (Result result : results) {
            if (result != null) {
              json.put("status", resp.getStatus());
              json.put("rawtext", result.getText());
              json.put("rawbytes", result.getRawBytes());
              json.put("format", result.getBarcodeFormat().name());
              json.put("parsedresult", ResultParser.parseResult(result).getDisplayResult());
            }
          }
          resp.setData(JsonUtil.toJson(json).getBytes());
        }
      }
      return resp;
    } finally{
      if (image != null) {
        image.flush();
      }
    }
  }

  private static final Map<DecodeHintType, Object> HINTS;
  private static final Map<DecodeHintType, Object> HINTS_PURE;
  static {
    HINTS = new EnumMap<>(DecodeHintType.class);
    HINTS.put(DecodeHintType.CHARACTER_SET, StandardCharsets.UTF_8.name());
    HINTS.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
    HINTS.put(DecodeHintType.POSSIBLE_FORMATS, EnumSet.allOf(BarcodeFormat.class));

    HINTS_PURE = new EnumMap<>(HINTS);
    HINTS_PURE.put(DecodeHintType.PURE_BARCODE, Boolean.TRUE);
  }

  private ArrayList<Result> processImage(BufferedImage image) {

    LuminanceSource source = new BufferedImageLuminanceSource(image);
    BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
    ArrayList<Result> results = new ArrayList<>(1);

    try {
      MultiFormatReader reader = new MultiFormatReader();
      ReaderException savedException = null;
      try {
        // Look for multiple barcodes
        MultipleBarcodeReader multiReader = new GenericMultipleBarcodeReader(reader);
        Result[] theResults = multiReader.decodeMultiple(bitmap, HINTS);
        if (theResults != null) {
          results.addAll(Arrays.asList(theResults));
        }
        Logger.info("Results：" + results.toString());
      } catch (ReaderException re) {
        savedException = re;
      }

      if (results.isEmpty()) {
        try {
          // Look for pure barcode
          Result theResult = reader.decode(bitmap, HINTS_PURE);
          if (theResult != null) {
            results.add(theResult);
          }
        } catch (ReaderException re) {
          savedException = re;
        }
      }

      if (results.isEmpty()) {
        try {
          // Look for normal barcode in photo
          Result theResult = reader.decode(bitmap, HINTS);
          if (theResult != null) {
            results.add(theResult);
          }
        } catch (ReaderException re) {
          savedException = re;
        }
      }

      if (results.isEmpty()) {
        try {
          // Look for normal barcode in photo
          Result theResult = reader.decodeWithState(bitmap);
          if (theResult != null) {
            results.add(theResult);
          }
        } catch (ReaderException re) {
          savedException = re;
        }
      }

      if (results.isEmpty()) {
        Logger.warn("Bar code not found.", savedException);
      }

    } catch (RuntimeException re) {
      // Call out unexpected errors in the log clearly
      Logger.warn("Unexpected exception from library", re);
    }

    return results;
  }

}
